Skip to content

fix(core): ObjectLogger file destination silently no-ops in ESM builds (#3110)#3116

Merged
os-zhuang merged 1 commit into
mainfrom
fix/logger-esm-file-destination
Jul 17, 2026
Merged

fix(core): ObjectLogger file destination silently no-ops in ESM builds (#3110)#3116
os-zhuang merged 1 commit into
mainfrom
fix/logger-esm-file-destination

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #3110.

What was broken

createLogger({ file }) never wrote the file from an ESM entrypoint. openFileStream loaded fs with a lazy require() to keep fs out of the browser bundle graph (the entry is deliberately browser-safe — @objectstack/client bundles it). esbuild rewrites that to its __require shim in the ESM output, which throws Dynamic require of "fs" is not supported, and the surrounding bare catch {} swallowed it.

The workspace is type: module, so this hit every Node ESM consumer — os serve, os dev — while the CJS build kept working. File logging has effectively never run in production.

The fix

  • Load the builtin via process.getBuiltinModule — a plain method call, so bundlers can't see it, working in both module systems, still behind the existing typeof process guard. require stays as a fallback for Node < 20.16, which predates it (engines allows >=18, so CJS on old Node must not regress).
  • A file destination that can't be opened now says so on stderr. The silence is precisely what kept this hidden, per the issue. It deliberately bypasses level filtering: this reports that the logger cannot honour its own config, which isn't an application log event.

Three faults this un-hid

Opening the destination for the first time made latent code reachable. Each is verified load-bearing by reverting it alone and watching the matching test go red:

Before Consequence
child() passed file into the eager-opening constructor, then overwrote the stream a second stream opened and orphaned per child — despite the no double-open comment
child destroy() ended the shared stream parent + siblings lose file logging from a child's teardown
async open failure no 'error' listener uncaught exception — an unwritable log path takes the host down

The third is the reason this isn't a one-line change: shipping only the require fix would have turned a silent no-op into a crash on a bad file path.

Verification

The issue's repro, against the built ESM dist:

$ node probe.mjs      # before
2026-07-17T11:27:18.312Z INFO esm file probe
file exists: false

$ node probe.mjs      # after
2026-07-17T11:41:36.072Z INFO esm file probe
file exists: true
contents: "2026-07-17T11:41:36.072Z INFO esm file probe\n"

Unwritable path — warns, keeps console logging, exits 0:

2026-07-17T11:41:36.099Z | INFO | console still alive
logger: file logging disabled — cannot write to /private/tmp/…: EISDIR: illegal operation on a directory
process survived, exit 0

Also checked: browser safety holds (no static fs/path import in the ESM dist; createLogger({file}) with no process global doesn't throw), @objectstack/client builds, @objectstack/core 24/24 test files pass with --force, eslint clean.

Note on the test

logger-esm-file.test.ts bundles the entry with esbuild and spawns a real node. That indirection is load-bearing: under vitest the source runs through vite-node where require is defined, so an in-process test of this code passes against the broken version. It fails on all 4 assertions at origin/main. It bundles rather than reading dist/ because turbo's test only dependsOn: ["^build"] (not its own build) and excludes dist/** from its inputs — a dist-reading test would be both unbuilt and mis-cached. This adds esbuild to core's devDeps (already in the lockfile via tsup; pnpm's strict linking wouldn't resolve it otherwise).

Rebased onto #3111, which landed on the same file and already makes the file destination plain-text — so no ANSI codes reach the now-working log file.

🤖 Generated with Claude Code

#3110)

`openFileStream` loaded `fs` with a lazy `require()` to keep the
browser-safe logger entry out of the `fs` bundle graph. esbuild rewrites
that to its `__require` shim in the ESM output, which throws `Dynamic
require of "fs" is not supported`, and the surrounding bare `catch {}`
swallowed it. The workspace is `type: module`, so every Node ESM
consumer — `os serve`, `os dev` — silently got no file logging at all,
while the CJS build kept working.

Load the builtin through `process.getBuiltinModule` instead: a plain
method call, opaque to bundlers, working in both module systems, still
behind the existing `typeof process` guard. `require` stays as a
fallback for Node < 20.16, which predates it. A `file` destination that
cannot be opened now reports on stderr rather than vanishing — the
silence is what kept this hidden.

Opening the destination for the first time also exposed three faults
that were unreachable while it never opened:

- `child()` passed `file` to the constructor, which opens eagerly, so
  every child opened a second stream to the same path and immediately
  orphaned it — despite the "no double-open" comment.
- Destroying a child ended the stream its parent and siblings were still
  writing to.
- `createWriteStream` reports open failures asynchronously; an 'error'
  event with no listener is an uncaught exception, so an unwritable log
  path would have taken the host process down.

The regression test bundles the entry with esbuild and spawns a real
`node`, because under vitest the source runs through vite-node where
`require` IS defined — an in-process test of this code passes against
the broken version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 17, 2026 11:45am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/m labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/core.

22 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/north-star.mdx (via packages/core)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core)
  • content/docs/kernel/services.mdx (via @objectstack/core)
  • content/docs/permissions/authentication.mdx (via @objectstack/core)
  • content/docs/permissions/authorization.mdx (via packages/core)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core)
  • content/docs/plugins/packages.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core)
  • content/docs/releases/v15.mdx (via @objectstack/core)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 5f05de2 into main Jul 17, 2026
16 of 17 checks passed
@os-zhuang
os-zhuang deleted the fix/logger-esm-file-destination branch July 17, 2026 11:57
os-zhuang added a commit that referenced this pull request Jul 17, 2026
…-build-approvals comments (#3125)

#3119 cited "#3110" in four comments as the pnpm 11 build-approvals bug. #3110 is an
unrelated, already-closed issue — "ObjectLogger file destination silently no-ops in ESM
builds" (fixed by #3116). The reference was fabricated: the pnpm 11 scaffold breakage was
found while diagnosing the red Publish Smoke gate and has no issue of its own, so the
correct citation is the PR that fixed it, #3119.

Comment-only. It left a wrong pointer in the two places most likely to be read next: the
"why is the smoke's pnpm unpinned" rationale in the script header, and the ratchet
guarding the template. The template pnpm-workspace.yaml and the changeset never cited it,
so nothing user-facing shipped with the bad reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ObjectLogger file destination silently no-ops in ESM builds

1 participant